Skip to content

design sweep 2: work the ranked backlog - #214

Merged
guarzo merged 23 commits into
mainfrom
worktree-design-sweep-2026-08-10
Aug 10, 2026
Merged

design sweep 2: work the ranked backlog#214
guarzo merged 23 commits into
mainfrom
worktree-design-sweep-2026-08-10

Conversation

@guarzo

@guarzo guarzo commented Aug 10, 2026

Copy link
Copy Markdown
Owner

A full design sweep of the app — eight surfaces in scope plus /admin/access-lists,
eighteen reviewer reports, and the fixes chosen off the ranked backlog in
docs/design-sweep-2/SYNTHESIS.md. That document is the record: what was found,
what it costs a user, what was worked, and what was deliberately left.

Nothing here changes a colour token, a migration, or persisted data.

What got fixed

Layout that fails on a phone. /payouts/[id] was the only surface whose
narrow capture was wider than the viewport — 771px against 390px — from two
independent one-line causes. /payouts' pinned Name column measured 69px of a
286px region, so the column that exists to keep a payout identifiable while
scrolling didn't; it now reflows to labelled blocks the way .log--crew already
does. /admin/sync's housekeeping summary had never fitted at 320px in any
state, healthy included.

Forms that promised things they couldn't deliver. Four of /payouts/new's
five error messages were unreachable through the form — native validation ran
ahead of the event React's <form action> fires from, so the app's own sentences
were server backstops for scripted requests only. And the three GET filter forms
used <Submit>, whose pending state cannot arrive during a document navigation
and whose guard latch is never released — a document that outlives its own
navigation came back with a permanently dead button.

Rejections that drifted away from their forms. The last fix in the branch:
a populated Notice renders a different class from the empty reserved slot, so
the spacing reset stopped matching at exactly the moment there was something to
say. Measured 0px empty, 88px populated.

Recital. /payouts and /admin/audit each printed one fact about the whole
set on every row. Both now state it once.

Six rules the design record states and the code disproved. A dedicated
reviewer read DESIGN.md against the implementation; drift never announces itself,
and some of it had survived multiple sweeps.

Plus accessibility fixes (three controls misreporting what they are, an inert
skip link, a retry that dropped focus to <body> on success), and
/admin/access-lists' fault states, which were reviewed unrequested but whose
findings lift out cleanly.

Verification

typecheck, lint, format:check, npm test (1476 passed, 92 files), the full
Playwright suite (407 passed), and npm run build — all run on the final tree.

Every test written to prove a fix was shown to fail with only the code under test
removed. That practice is now written into AGENTS.md as item 22, after a control
run in this branch turned out to be inert and to have been passing for the wrong
reason.

What was found and not worked

Recorded in SYNTHESIS.md rather than silently dropped:

  • /admin/audit's Details column prints raw character IDs where the Target
    column three cells left resolves the same referent to a name. Real scope — a
    new key map, a new part, a widened fetch. Being handled separately.
  • payout.deleted clips mid-number at 1440px; the disclosure recovers it.
  • At 390px the Details column is entirely off-screen behind its scroller.
  • The #0a0a0a ground is PRODUCT.md's own anti-reference, and the whole-app
    reviewer ranks it third by cost. Outside the colour freeze this sweep ran
    under, so it is surfaced for a decision rather than acted on.
  • Three items argued to sit outside the freeze — a stale themeColor literal
    from the retired navy palette, paid and unpaid rendering at the same
    colour on the roster, and .escalation's 1.62:1 border. All are token use
    or assignment changes, not value changes. Owner's call.

Summary by CodeRabbit

  • New Features

    • Improved responsive layouts for payouts, rosters, access lists, audit, and synchronization pages.
    • Added clearer grouped scope descriptions on the login page.
    • Added accessible focus handling and recovery after errors.
    • Added clearer pending-action labels for payout operations.
    • Added concise payment-history summaries and responsive table labels.
  • Bug Fixes

    • Improved validation for future payout dates and invalid report links.
    • Fixed disclosure labels, focus visibility, artwork sizing, and mobile overflow.
  • Documentation

    • Added comprehensive design-sweep findings, recommendations, and accessibility guidance.

guarzo added 21 commits August 10, 2026 10:04
…etry

Two defects that shared a shape: a message the user could never get to.

- The error boundary's "Try again" called `reset()` alone, which re-runs the
  segment from the client router cache -- still holding the payload that threw.
  A server-side failure therefore rendered the same error forever. Pairs it
  with `router.refresh()` so the cached payload is discarded first.

- Native constraint validation runs before the `submit` event, and React's
  `<form action>` runs from it, so `required` / `type="url"` / `max=` on the
  new-operation form made its server-side rejections unreachable. Removes the
  native attributes that shadowed a server message and corrects the copy: a
  bare hostname fails `new URL()` outright and yields `url_invalid`, not the
  `url_scheme` the old text described.
Below 30rem the sticky name pin measured 24% of the region, so the column it
anchored was wider than the space left to read it in. The row now reflows to
labelled blocks, the way `.log--crew` already does, and `.payouts__label` /
`.roster__label` name the columns the missing `<thead>` no longer can.

Sibling classes rather than a reuse of `.crew__label`: the two tables reflow at
the same breakpoint today and a shared class would weld that together. The
register entry is the part worth sharing, and it is shared.

The pin's ratio assertion moves from 320/390px to 560px, where there is still a
pin to measure -- a `<td>` laid out as a block has no scroll range.
Access lists: "Check now" rendered in every state, including the one a fresh
deployment opens on -- no holder, so the job returns at its first branch having
read nothing, and the admin was told "Check queued at 09:41:22.418 UTC" and
reloaded to a byte-identical page. The confirmation was true about the enqueue
and false about everything they cared about. Gated on `remedy.kind ===
"check-now"`, which is tighter than `showsObservations(state)`: the job cannot
read in the three holder-fault states either, and an e2e assertion on state 3
pins that divergence.

Payouts detail: eight confirm controls whose actions revalidate rather than
redirect, so a press moved nothing on screen for the whole round trip while
`useSubmitGuard` silently refused every further press -- and with no
`ConfirmGroup` on this route there is no channel for the refusal. Each now
carries a `pendingLabel`, which is where the operator is already looking.
`LifecycleSubmit` takes it as a required prop so a new lifecycle control has to
answer the question rather than inherit the gap.

`ConfirmSubmit`'s ghost-label width reservation was widened to include a string
`pendingLabel` first, so the new labels cannot reflow a button mid-press.

Also carries the roster's `Shares` per-cell label, which cannot be split from
this commit without leaving a tree that does not compile.
…answer

The audit log writes a member as `actor` only for what they did to their own
account. Everything done TO them -- role changes, tier moves, unlinks -- is
written with `system` or an admin as actor and the member as target, and that
is most of the log.

The page already knew this and said so, from inside `emptyMessage`'s filtered
branch: it fired on zero rows, the one outcome that has already told the admin
something is wrong. The outcome that does the damage is the other one -- an
actor filter that returns the member's handful of self-service entries, reads
as a complete history, and is not one. Same sentence, hoisted above the rows so
both cases see it.

Still gated on actor-set-and-target-unset, the only shape it can help. Rendered
as a plain `.lede`, not a Notice: nothing here is wrong, the filter is just
pointed at the column that answers a different question, and a warn band would
say otherwise.

e2e/audit.spec.ts asserted the old placement; rewritten to walk all four states
-- no filter, actor filter empty, actor filter with a row, both columns crossed.
Six rows at one identical weight, of which exactly one -- write_contacts --
writes anything at all, sitting second of six with nothing marking it out. And
six sentences that each re-established "so authGD can show your location",
teaching the reader that these lines do not differ, on the one screen where the
differences are the entire point.

crewNorms' shape from account/page.tsx: the shared fact once in the group's
head, the rows carrying only what differs. Three groups -- contacts (the only
thing authGD writes), location, running client -- each a <dl> of its own, so
the head is adjacent to exactly the rows it describes in both channels rather
than being a <p> stranded inside a single list with no defined relationship to
either side of it.

Grouping reorders relative to EVE_SSO_SCOPES on purpose: config order is an ops
concern and means nothing to the person deciding. A scope the list does not
place still renders, last, under a head that admits the deployment added it;
empty groups do not render at all.

The head is --ink at weight 600 and sentence case -- deliberately not a fourth
member of the uppercase label register, which is for fixed field names, not
sentences. That makes it the brightest text in the disclosure, which is the
ordering wanted: the answer to "does this change my stuff" lands before any
identifier.

Two more, same surface:

- `.launch__panel > .notice` gets a margin-top. `.notice` carries only a
  margin-bottom, right everywhere it follows a `.page__head` and wrong here,
  where it follows the emblem/title/motto stack directly and sat flush against
  the motto. A member bounced back by an expired session met the reason for it
  welded to the brand mark. Scoped to the child selector so the six other
  surfaces are untouched, and `.notice-slot` -- the empty live-region
  reservation this page mounts unconditionally -- is a different class and
  still draws nothing.

- The emblem gets an explicit fetchPriority="low". It is 79.8 KB of decorative
  WebP with alt=""; the control below it is 2,248 bytes carrying an explicit
  high. Left unset the emblem contends for the same connection on a first visit
  over a bad link, and the page's one actionable thing waits behind its own
  background.

e2e/login.spec.ts updated for the new order and wording, plus assertions on the
group heads and on head > description > identifier luminance. The fallback
group is not covered: EVE_SSO_SCOPES is fixed by playwright.config.ts, so an
unplaced scope cannot be rendered from a spec. The head count of 3 pins that it
does not appear for the deployed set.
Pattern 2 of the sweep, two of its five instances.

payment-history: a real payout is settled in one sitting by one operator,
so six rows read `2026-08-10 11:57:07 UTC paid 288,600,000.00 ISK by Fleet
Commander` six times over, and the two things an operator is scanning for —
the clock and the amount — are the two smallest fragments of the line. The
norms are measured against the set (crewNorms' shape from account/page.tsx):
a field is dropped only when every event agrees, so one reverted row among
five paid ones puts `kind` back on all of them rather than removing it from
four. The single-payment case renders inline with no drawer and therefore has
no head to hoist into, so it keeps the whole sentence.

audit: the timestamp column is pinned, 19 characters wide, and eleven of them
restate a date on a page whose rows almost always fall on one day. When they
all agree the day is stated once above the table and the cells render a bare
clock; when they do not, nothing is hoisted and every row carries its own.

Both channels lose exactly the same words in both places. The shared line
renders in the flow, not in a `visually-hidden`, and `.only-wide` is a display
toggle so the trimmed audit cell is what AT reads at that width too — R4 is
parity in both directions, and a per-row assistive-only restoration would be
the breach it exists to name.

The audit spec's two width assertions accepted only the full stamp; they now
accept either rendering, since which one appears is the new test's subject and
not theirs. That test drives both branches, because the interesting failure is
not the hoist but the hoist firing on rows that disagree — which would delete
a date the admin needs and state a false one above it.

Verified: npm run typecheck clean; npm run format:check clean;
vitest tests/payment-history.test.ts 5 passed;
playwright e2e/audit.spec.ts 38 passed; e2e/payouts.spec.ts 103 passed.
Item 5 — /admin/sync's housekeeping summary has never fitted at 320px.

The filed fix was to add `.strip__group-disc > summary` to the
`@media (max-width: 46rem)` block so it gains `flex-wrap: wrap` the way
`.strip__disc > summary` does. That would have done nothing: the summary
holds exactly one child (page.tsx:1086-1094, a single `<Status>`), and
wrapping a flex container with one item wraps nothing. The nowrap is on
`.st` (globals.css:2486-2497), which is written for a one-word token in a
table cell and is right everywhere else it lands.

So the wrap is granted to the Status, scoped to this summary, plus
`align-items: baseline` on the summary itself — with the sentence on three
lines, the base rule's `center` puts the `+` marker halfway down the block
instead of beside the line it opens. The absolute-gutter treatment
`.strip__disc > summary` takes was not copied: that exists because
`.strip__name` claims a 100% basis and strands the marker, and nothing
here does.

Not truncated. The flagged member names are the only thing the collapsed
line exists to say.

Item 19 — four page-level disclosures took the 28px in-row grade.

`.disc > summary` raised to `min-height: 2.25rem`. R1 scopes the 28px
grade by the reason for it and states the consequence outright: "A
disclosure drawer is not in-row for this purpose and takes 36px."
`className="disc"` has exactly four call sites, all page-level sections of
/payouts/[id] (page.tsx:135, :844, :1165, appraise-form.tsx:183).
PaymentHistory is unaffected and should be — `Disclosure` passes
`className` through with no default (disclosure.tsx:144), that call site
passes none, and `.log summary` styles it where 28px is correct.

Verification, all run:
  npm run typecheck            clean
  npm run format:check         All matched files use Prettier code style!
  playwright e2e/sync.spec.ts       33 passed
  playwright e2e/payouts.spec.ts   104 passed

The 320px test is self-verifying: it puts `.st`'s own nowrap back on the
element and asserts the line then demands more width than it was given, so
it fails if the rule is dropped and also fails if the rule never did
anything. Confirmed against the unfixed stylesheet: 340.86px of line in a
320px viewport. The containment assertion measures the `.st`, not the
`<summary>` — an overflowing flex item spills past its container without
growing it, so asserting on the summary's right edge passes vacuously.
Item 7 — `Notice` renders a `<p>` inside a `display: flex` container, so
every top-level child of the message became its own flex item, laid out in
the row box independently of the inline content it contained. A caller
writing `Quote <code>{digest}</code> when you report this` got the `<code>`
promoted to item 2 and the two text runs to items 3 and 4, and the rendered
order read "Quote 4292868890 . when you report this". Visual order diverging
from DOM order is SC 1.3.2, and it is invisible in the source. `{children}`
is now wrapped in one `<span>`, which fixes every `Notice` in the app
carrying inline markup at once.

Item 10 — `/admin/audit` had a static title, so filtering and paging changed
the whole result set without changing anything an admin could hear. The
filed fix was `aria-live`; it does not work here. Every state change on that
surface is a document load (`<form method="get">`, plain `<a href>` pagers),
and a live region arriving with its document is not a mutation, so it
announces nothing while testing green under an attribute-presence assertion.
`generateMetadata` instead: the title says "filtered", "older", or both. It
applies the same finite-number guard to `before` that the page body does, so
a junk cursor cannot make the title claim a page the admin is not on.

Item 11 — `/admin/access-lists` was the one `id="main"` of eleven without
`tabIndex={-1}`, having landed after the sweep that added the rest. A
fragment link moves focus only to elements the platform already considers
focusable, so the skip link scrolled without moving the caret and the next
Tab walked back through the nav (SC 2.4.1).

Item 18 — two row disclosures derived their accessible name from contents
that included `RelativeTime`, a client component on a shared 30s ticker. The
controls renamed themselves twice a minute with no state having changed:
SC 4.1.2 in the announcement channel, and SC 3.2.4 for a voice user whose
"click Alliance ACL 3 to add 2 minutes ago" stops matching what the page now
calls it. Both get a server-computed `ariaLabel`. Note that `aria-label` on
a `<summary>` replaces the computed name outright, so everything stable the
contents were contributing is restated by hand — dropping it would be R4
inverted, deleting information from the assistive channel only. Only the
timestamp is left out, which is the entire point.

Also fixes a regression from 9215e1f: that commit inserted a comment block
between login's `eslint-disable-next-line` and the `<img>` it guards. "Next
line" means the next line and a comment is a line, so the directive
retargeted onto the comment and unguarded the element. Caught by lint, not
by review.

Tests. Three of the four fixes needed a control experiment, because the
obvious assertion for each passes whether or not the defect exists:

- The stranded-period measurement in error-boundary.spec.ts was run against
  a restored `display: contents` — "Expected: < 2, Received: 12", 1 failed.
  That also convicts the two `toContainText`/`toBeVisible` assertions that
  were already there: DOM text order never changed, so they could not have
  seen this.
- The skip-link test was run with `tabIndex={-1}` removed —
  `Expected: "main", Received: ""`.
- The two rename-stability tests are self-guarding: without the prop
  `getAttribute` returns null, which the non-empty precondition catches, and
  each asserts the `.ago` text changed so a page where the clock never
  ticked cannot pass vacuously. They test stability only; what the name
  *contains* stays pinned by the existing `toHaveAccessibleName` cases.

Verification, all run:

  npm run typecheck    clean
  npm run lint         clean (was 2 warnings before the login fix)
  npm run format:check All matched files use Prettier code style!
  npx playwright test e2e/{sync,audit,access-lists,error-boundary,login}.spec.ts
                       99 passed (1.3m)
…k like

Item 17. `globals.css` keeps one list of selectors that share the label
register — mono, `--t-label`, uppercase, 600 — and DESIGN.md states it in
absolute terms. `.status-line__label` was shaped exactly like a member and
was not one: the only register-shaped selector in the file that never
declared a weight, so it fell to the 400 default.

It surfaces on /account because both channels render the same words. The
rail's `.facts dt` says STANDINGS and MAP; the manifest's status stack says
STANDINGS and MAP. Same family, same size, same colour, about 430px apart,
two different weights. Nothing tells a reader the difference is meaningless,
so the page implies a hierarchy it does not have — which is the precise
collision the register block was written to close.

Added to the list; the rule keeps only its own `letter-spacing` and `color`,
which is what every other member also keeps for itself. Three duplicated
properties go. (SYNTHESIS says four — it counted `letter-spacing` or `color`
as shared, and they are not.)

No layout cost, and the reason is worth recording: the register is
monospace, and IBM Plex Mono ships 400 and 600 at the same advance width, so
weight is free here in a way it would not be in the body face. Measured
rather than assumed — the STATUS cell at 320px forced scroll reads
223.140625px, identical to the figure already documented at
account.spec.ts:1487. The 275-299px range documented at :146 and the 232px
gate are all untouched, so no prose went stale.

That identical measurement is also why the fix needed proving rather than
inferring: "the width did not move" is exactly what a change that never took
effect looks like. Probed the computed style directly and got 600.

New test asserts parity between the two channels rather than the literal
600, because parity is the requirement and the constant is only today's
value of it — if the register moves, the test should follow it rather than
fail. It reads both from the live page, since the way this bug arrived is
that every property present was individually correct and one was simply
absent, which no source-level read of the rule notices. Control experiment:
with the CSS reverted, "Expected: 600, Received: 400".

Verification, all run:

  npm run typecheck    clean
  npm run lint         clean
  npm run format:check All matched files use Prettier code style!
  npx playwright test e2e/{account,location}.spec.ts
                       86 passed (56.3s)
Item 14 of the sweep, error boundary only. Both controls in `.btn-row` are
the plain grade and sit 8px apart, so position is the only thing separating
them — and the control in first place, where a reader takes the offered
answer to be, was the one the lede directly above had just warned about:
"your action may already have taken effect; check before sending it again",
followed immediately by the control that sends it again.

Reversed the two rather than taking the synthesis's first-listed fix.
`.btn--quiet` on Try again is unusable here: that class carries
`min-height: 1.75rem`, and DESIGN.md R1 scopes the 28px grade by the reason
for it — rows that each carry a control set and are read many at a time.
Two buttons read once are not that, so the quiet class would have bought
the differentiation by violating the grade rule. Recorded because the same
objection blocks the same fix on /admin/sync's Refresh.

Not differentiated upward: gold is the page's one emphasis ration, and a
boundary has no action it can recommend, so spending it on either control
would be a claim this page cannot make. Gold on Try again stays settled and
unreopened. Order is the free axis, and it fixes the tab order by the same
move.

Regression test asserts DOM order via compareDocumentPosition rather than
x-coordinates: `.btn-row` wraps at narrow widths, so the leftmost control is
not stably the first one, and the tab order follows the DOM either way.

Control experiment, because five assertions this sweep have been vacuous:
with only src/app/error.tsx reverted, the spec fails —
  Error: Try again sits before the escape route
  Expected: true / Received: false
Restored, it passes.

Verification, all run:
  npm run typecheck        clean
  npm run lint             clean
  npm run format:check     All matched files use Prettier code style!
  playwright e2e/error-boundary.spec.ts   8 passed (8.2s)
… the same

Item 14 of the sweep, second and third instances. `error.tsx` was the first
(ec10520); this closes /admin/sync and /payouts/[id].

/admin/sync: Refresh sat in `.btn-row--controls` as a third peer, an `<a href>`
with no pending state drawn identically to `Recheck invalid affiliations` 8px
away, which enqueues a job. One changes nothing, one puts work on the queue, and
only the label separated them. It now sits in `RuleHead`'s aside beside the
"checked … UTC" stamp — its actual subject — leaving the control row holding
exactly the two things that enqueue.

Differentiated by adjacency, not grade: `.btn--quiet` carries
`min-height: 1.75rem` and DESIGN.md R1 (:364-369) scopes that 28px grade by the
reason for it — rows each carrying a control set, read many at a time. A single
control in a section header is not that. Gold is spent on `Sync now`; a second
would flatten the first.

`view.ts`'s `queuedNotice` docblock argued for "reload this page" over "use
Refresh" on distance — Refresh being at the foot below seven rows. My move
invalidated that reason, so the docblock is rewritten to stand on the surviving
one (a reload needs no lookup and works whether or not the anchor rendered).

/payouts/[id]: 70 pressable things, 62 of them the identical 28px quiet chip.
New `prominentTrigger` prop on `InlineEdit` raises the trigger to a plain `.btn`,
applied to the operation's name and date only — the two fields that are the
record's identity rather than a field of it, and the only two above the fold.
Deliberately not all five `standalone` uses: promoting five of 62 makes a second
uniform tier, the same defect one notch up. Costs no layout — bare `.btn` already
carries the `min-height: 2.25rem` that `.inline-edit--standalone .btn--quiet`
buys back — so it changes fill and border only, and adds no second gold.

layout.tsx: `themeColor` still held `#080f1f`, a navy from a palette the app no
longer uses, while `--void` is `oklch(0.145 0 58)`. On mobile that painted blue
browser chrome above a neutral page. Corrected to `#0a0a0a`: chroma 0 means equal
channels, 0.145³ = 0.00304862 is below the 0.0031308 sRGB knee, so
12.92 × 0.00304862 × 255 = 10.04 = 0x0a. This is drift in a hardcoded duplicate
the viewport meta cannot read dynamically, not a palette change — the token is
untouched.

NOT DONE, deliberately: the synthesis also asked to demote the per-row `exclude`
control on /payouts/[id]. Every control in that action cell (page.tsx:1020-1100)
is already `btn btn--quiet btn--micro`, which is the floor. Reaching lower needs
a new CSS grade below the one R1 already scopes, so it is recorded rather than
invented.

Verification, all run:
- typecheck: clean
- lint: clean
- format:check: "All matched files use Prettier code style!"
- e2e/sync.spec.ts: 35 passed (29.5s)
- e2e/payouts.spec.ts: 105 passed (1.6m)

Control experiments on both new tests, per the discipline this sweep has been
using — the fix reverted, the test re-run, the failure confirmed:
- sync: `Expect "toBeVisible" ... waiting for locator('.rule-head__aside')
  .getByRole('link', { name: 'Refresh' })` — 1 failed
- payouts: `locator resolved to <button ... class="btn btn--quiet btn--micro">
  - unexpected value "btn btn--quiet btn--micro"` — 1 failed

The payouts test's `notRaised` counterweight is `edit battle report URL` alone.
Corp share was tried first and failed: its row only renders once a loot pool
exists, and seeding one would test the fixture rather than the grade.
…ed ones

Item 20 of the sweep: comments and design-record lines asserting something the
code or the palette contradicts. Nothing here changes a rendered pixel except
one CSS rule; the rest is the record catching up to what is true.

The contrast numbers were all re-measured, and the instrument mattered more
than the arithmetic. `getComputedStyle` hands these tokens back as `oklch(...)`
— still in their authoring space — so the obvious probe, which parses the
string for numbers, reads L, a and b as r, g and b. It reported `--ink`, a
near-white, as `#5c0005`, and every ratio in the palette as between 1.01 and
1.47, without failing. Painting each token to a 1x1 canvas and reading the
pixel back is an actual sRGB conversion; those are the numbers below. The first
run's output was discarded, not filed.

    --void #0a0a0a  --hull #151514  --hull-hi #21201f
    --rule #373533  --rule-strong #787370
    --ink #ece7de   --ink-dim #bab3a9  --ink-faint #90877e  --gold #f1c035

What the record claimed, and what it measures:

    --ink-faint on --hull        5.58  ->  5.18   (globals.css x2)
    --ink-faint on --hull-hi     4.85  ->  4.61   (globals.css x2, DESIGN.md)
    --rule-strong on the three   4.24/3.92/3.48 -> 4.23/3.90/3.47
    ...and at a second site      4.11/3.72/3.24 -> same
    --rule on the three          1.76/1.59/1.39 -> 1.62/1.50/1.33

No floor moves. 5.18 and 4.61 both clear WCAG's 4.5:1 for text, and
--rule-strong still clears 3:1 on every ground a control edge can sit on, which
is the whole reason its lightness is pinned. Every argument these numbers were
quoted in support of survives; only the digits were wrong. Colour tokens are
untouched.

Two of those were already contradicting each other in-tree. `--ink-faint` on
--hull-hi read 4.85 at three sites and 4.63 at two others; the measurement
resolves it at 4.61, so the 4.63 sites were right and the 4.85 ones were not.
`--rule-strong` carried 4.24/3.92/3.48 at its own definition and 4.11/3.72/3.24
sixty lines from the rule that cites it. All sites now quote one measurement,
and the file header records how it was taken so the next re-measure is
comparable rather than a fresh guess with a fresh instrument.

The other claims, each checked against the code rather than against the report
that flagged it:

- `focus-heading.tsx` said a programmatic focus draws no ring, because
  `:focus-visible` cannot match one. Chromium does match it: measured
  `matches(":focus-visible") === true` and a 2px solid outline painted. The
  belief had been copied into `confirm-notice.tsx`, so both are corrected.

  The ring is kept — it is the sighted half of the announcement those
  components exist to make, and on a soft navigation into a boundary the focus
  move is the entire point. What was wrong was its size: an `h1` is a block, so
  the ring spanned the column, 912px around 377px of text. That reads as a
  page-wide gold band, not as a focus indicator.
  `h1[tabindex="-1"] { width: fit-content }` hugs the text. Unconditional
  rather than `:focus`-scoped so nothing shifts as focus lands, and it costs no
  wrapping — `fit-content` is `min(max-content, max(min-content, available))`,
  so wherever the heading would wrap it is still the available width and
  `text-wrap: balance` is unaffected. `ConfirmNotice` gets no such rule: it
  wraps a full-width panel, where a full-width ring is the right size.

  This is the one behavioural change in the commit, and it is the one thing
  here a comment correction turned into a real defect fix.

- `nav-items.ts` said `navFromPath` serves "the three surfaces". It serves one.
  Both `not-found.tsx` files pass hardcoded `navFor(...)` literals instead — a
  404 knows its route statically in a way a shared error boundary does not. A
  reader trusting "three" goes looking for two call sites that never existed.

- `submit-guard.ts` justified refusing `disabled` with "every one of these
  actions ends in a server-action `redirect()`". `syncJobAction`
  (admin/sync/actions.ts:67) revalidates and returns, driven by a `<Submit>`
  like the rest. The narrower claim is true and covers both shapes: neither a
  client `redirect()` nor a `revalidatePath()` restores focus. The wrong
  version invited the wrong repair — adding redirects to the actions that lack
  one would not have helped.

- `.launch__scopes dd` said an unrecognised scope "renders no dd at all".
  `describeScope`'s default branch returns a sentence saying the deployment
  asks for the scope, authGD cannot describe it, and to ask whoever runs it.
  That is the better behaviour as well as the actual one, and login/page.tsx
  points readers at this comment by name, so the false claim was signposted.

- `.btn-row--controls` described its position as "follows the data it operates
  on". True on /admin/sync; on /admin/access-lists it is the first thing under
  the H1, above the data, because the control it holds is a remedy for a list
  that may not have rendered. The margin is the rule, the placement is the call
  site's.

- `confirm-submit.tsx` claimed no caller keeps one class in both states. Five
  do, by construction — `armedClassName` is optional. The claim that holds is
  the narrower one about `.btn--danger` at rest.

- `payouts/new/page.tsx` credited `--hull` for boxing the form. `--hull` on
  `--void` is 1.08:1. `.form-panel`'s 1px `--rule-strong` border, at 4.23:1,
  draws the box; the ground sets field contrast without competing. They are
  load-bearing in opposite directions, and dropping the border to "flatten" the
  panel would delete the box rather than soften it.

Verification. `npm run typecheck`, `npm run lint` and `npm run format:check`
all clean. `e2e/not-found.spec.ts` and `e2e/login.spec.ts`: 12 passed. The new
`fit-content` test was control-verified by reverting only the rule, keeping the
test — it failed on `expect(912).toBeLessThanOrEqual(379)`, which is the exact
before-measurement the comment cites, then passed again on restore. Stashing
the whole change would not have been a control here: it removes the test along
with the fix.

Not done: the 65% / 3.24:1 / 2.88:1 figures in the `.btn:disabled` comment and
DESIGN.md's matching bullet describe an opacity that no longer exists in the
code. They are a historical justification for why the explicit colour replaced
it, the direction of the argument is right, and there is nothing live left to
measure them against.
Item 21, the record-contradiction reviewer's remaining findings. Every one is a
written rule that the implementation has never followed — and in five of the six
the implementation is right and the rule is wrong, which is the direction that
makes these worth fixing rather than filing. A rule nobody obeys is not
harmless: it is an instruction to break working code, sitting in the file a new
contributor reads first.

- **Gold ration.** The rules list said "one primary action per view, plus the
  mark" — two uses. The `--gold` token's own row, twenty-five lines above, lists
  four: brand mark, active nav, primary action, Member tier. The app matches the
  four. Someone auditing a screen against the short version finds gold on the
  nav and on a tier badge and has to pick which is the mistake, when neither is.
  The focus ring is gold too and is now called out as exempt — WCAG 2.4.7 puts
  it wherever focus lands, and an indicator cannot be held to a budget.

- **"No decorative gradients at all."** Two `linear-gradient`s exist. Neither is
  decorative, which is why they survived: `.scroller-fade--start` / `--end` are
  the only rest-state cue that a table continues sideways, and `.status__dot`'s
  neutral bar is a gradient of one colour against itself — a flat fill, used so
  the 0.15em mark sits in a constant-height box and every tone shares a
  baseline. The rule now says what it means. Read literally it asked for a
  deleted scroll affordance and a misaligned status row.

- **Nav membership.** DESIGN.md listed five destinations; `navFor` returns six.
  `Access lists` was missing. The module's own docblock has had all six since it
  shipped, so this was the record lagging the code by one feature.

- **`--signal-ok`.** Declared, documented, consumed by nothing, and both DESIGN.md
  and globals.css pointed at `.notice--ok` as the place it lives. That class has
  never existed. An editor following the citation finds two comments naming it
  and no rule. Kept rather than deleted, and both sites now say why: the notice
  family has a visible gap where an ok variant goes, and `--tier-alumni` holds
  the same value, so deleting the token leaves the next ok notice reaching for a
  tier colour.

- **`--dur-move`.** Also dead, and the reason is more interesting than the fact.
  All ten `transition` declarations animate a colour, a border or an opacity,
  and every one takes `--dur-color`. Transforms do exist — `seal-settle` at
  620ms, `btn-pulse` and `link-pending-pulse` at 900ms — but they are
  `@keyframes` and each names its own duration, because a settle and a loop are
  not paced like a hover. So the 220ms step is a reservation for the first
  transform *transition*, and both the token and the motion section now say that
  instead of implying the scale is in use.

  Checked rather than assumed: an earlier draft of this commit said "nothing in
  the shipped UI moves", which the three keyframes disprove.

- **"Never a large file scaled down."** PRODUCT.md's fifth principle banned
  exactly what the account page does and should keep doing.
  `hero-account.webp` is a 1120px master drawn at 420, or 260 under
  `.closing--compact`. That oversampling is what keeps it crisp at 2x. The
  failure the principle exists to prevent is the other direction — an asset
  stretched past its cut, or a fragment cropped for decoration — and stating it
  as a downscaling ban condemned the correct implementation while leaving the
  real failure unnamed. Two comments repeated the inversion: `.closing` claimed
  the lander was "drawn at the size it was cut for", and the account page
  claimed the compact variant asked for "a smaller frame rather than
  downscaling", which describes the same operation as its own opposite.

Two live changes, both provably inert: `letter-spacing: 0.12em` at `.tier--lead`
and `.launch__foot` becomes `var(--track-label)`, which is declared as `0.12em`.
Same computed value, so a test asserting the rendered tracking would pass with
or without the edit and prove nothing — the argument is the substitution, not a
measurement. Everything else in this commit is prose.

`npm run typecheck`, `npm run lint` and `npm run format:check` all clean.
`e2e/account.spec.ts`: 82 passed.

Not done: `<Image>` on the account page declares 1120x711 with no `sizes`, so a
260px frame still fetches the full master. That is a real cost and it is a code
change with a visual risk, not a record correction, so it stays out of an
item-21 commit rather than riding along in one.
… a fix

Item 22. Record-only: no code, no lint rule, nothing that runs. It is a
paragraph because the failure it prevents is one of judgement, and there is no
static check that can tell a load-bearing assertion from a vacuous one.

The finding behind it: six assertions written during this design sweep passed
for the wrong reason, and every one was caught by removing the fix and watching
the test stay green. Review caught none of them. That is the whole argument —
a new test that passes tells you nothing about whether it can fail, and a green
run reports "the fix works" and "the assertion is inert" in identical words.

The section also records the instrument, because getting that wrong is how the
check silently no-ops: reverting the whole uncommitted change is not a control
when the test is part of that change. Stashing takes the test away with the fix,
nothing runs, and the resulting green is read as confirmation. The revert has to
be surgical — the code under test only, assertion left standing.

Three worked examples from this repo, since the failure mode is easier to
recognise than to describe: `toBeVisible()` against `.visually-hidden` (a 1px
clipped box still has a bounding box), a row-count assertion on a table whose
empty state is also one `<tr>`, and a colour assertion parsing
`getComputedStyle` output when these tokens serialize as `oklch(...)` and a
naive parse silently reads lightness as red.

`npm run format:check` clean. Nothing else to run — the file is prose.
Twelve cases, 26 PNGs at 1440x900 and 390x844, from one temporary spec that
is deleted again. Both gaps were the same complaint: findings reasoned from
markup, on states nobody had rendered.

Gap 3 closes clean and that is the result. /login?error= paints the two
recoverable codes neutral and the two failures red, and an unrecognised code
paints nothing at all rather than an empty bordered box. loginErrorTone's
docblock argued this; the shots are the first evidence for it.

Gap 5 does not close clean. Four findings, three of which are the sweep's own
named patterns arriving together on one page:

- Every fault state is an unshaped field — a ~1200px rule labelling a ~545px
  notice with 500px of void beneath it.
- showsObservations excludes only the two states with no holder, so four
  broken-holder states still print "No lists are being watched yet." under a
  lede that already explained the fault.
- The missing-access table recites "Null Harvest Inc" on 8 of 10 rows, and
  four broad-grant lines end in the same seven words. crewNorms applies.
- STOP WATCHING renders at rule-label weight, identical per row, and is the
  only destructive control on the page.

Plus one plain defect: a watched-but-never-read list renders as "#4104"
though the catalog holds its name and catalog is already in scope.
watchedListName already does catalog-then-snapshot with a docblock saying a
missing name must never cost the row — wired to the audit writes and not to
the display path.

Record only. No code touched. npm run format:check clean.
The shots are regenerable from capture.spec.ts.txt and run to several
megabytes of PNG. docs/design-sweep/ set the precedent: reports tracked,
pixels not.
…reciting itself

Five findings from the second design sweep's re-shoot of this page, all on
/admin/access-lists.

The three holder faults put one sentence and one link in a 78rem column. They
now take `page--narrow`, which caps contents rather than the page box, so the
h1's left edge and every rule's origin stay on the same vertical as the other
admin routes — only the line length changes.

Those same states rendered a "Watched lists" heading over a notice saying the
heading had nothing under it. `showsObservations` is right that a stale answer
beats a blank page, but only when there are rows; `showsRegion` adds the second
half. The `ConfirmGroup`/`ConfirmingForm` pair deliberately stays OUTSIDE that
gate — removing a list that has since left the catalog empties `compared` and
`addable` in the same commit, so gating the pair would unmount the reporter on
exactly the press it exists to report (the failure documented at
admin/accounts/page.tsx:1075-1097).

The "missing access" table printed one Corporation per row, which on the common
case — one corp's members left off an alliance list — told the rows apart not at
all. `sharedCorporation` measures the norm against the set the way `crewNorms`
does on /account: state it once, list only what differs, and defeat the norm on
a single unplaceable row rather than folding it in, because the sentence says
"all of them". The broad-grants "plus an unknown number of others" clause moves
out of the per-line map for the same reason; the wording is unchanged, so the
promise is textually identical.

"Stop watching" is the only control in a watched row, and `.btn--quiet` strips
the border that says so — at rest it sat on the weight of the captions around
it. Outline back, `.btn--quiet`'s restraint kept. No colour token added or
changed; --rule and --ink-dim already existed.

Also: a list watched but not yet read has no snapshot, so its row printed a bare
`#4001` while the catalog it was picked from knew the name. Both now derive from
the catalog-first rule `watchedListName` already used on the audit path.

Six e2e tests, each shown to fail with its fix surgically removed (AGENTS.md
item 22) — hardcoded page--wide, `showsRegion` widened to `showsObservations`,
the catalog lookup dropped from `label`, `missingCorp` pinned to null, and the
border-color declaration deleted.

npm test 1476 passed. npx playwright test 405 passed. typecheck, lint,
format:check, build all clean.
…t reach

`<Submit>` exists for server actions. In a `<form method="get">` the submit
is a document navigation, so `useFormStatus()` never reports pending: the
button's `aria-busy` is fixed at "false" for the life of the document, and
the guard's latch — taken synchronously on the first click, released only by
watching pending go true and back — is never released. The ordinary path
replaces the document before that matters, but a document that outlives its
own navigation comes back with the latch set and the button silently
refusing every press.

The three GET filter forms get a plain `<button type="submit">` instead.
Nothing rendered changes: they were passed the same className, and the only
other output was the aria-busy that was always "false". They lose nothing
either — re-running a filter is idempotent, which is why the guard had no
work to do there in the first place.

Also corrects the audit page's docblock, which named `Submit` as the reason
that page is not JS-free. `Scroller` is.
…iewport

`next/image` decides what to pull out of the srcset from `sizes` alone, and
with none it assumes the image spans the viewport. The closing artwork draws
at 420px (260px in the compact layout) and was being fetched at 1200 on a
desktop — the heaviest asset on the page, at roughly three times the width it
is drawn at, and worse on a phone where it matters more.

The two values track `.closing img` and `.closing--compact img`, so the
branch is computed once above and drives both the class and the hint; a
`sizes` that outran the CSS would undo the point of the narrower frame.

The test asserts the width the browser chose out of the srcset rather than
the attribute, because the attribute is the input and the fetch is the cost.
Control run with the prop removed: 1200 against a bound of 640.
Gaps 1, 2 and 4 were the shots the sweep never got. Two fixture
corrections were all that stood in the way, and both were the same
mistake — a fixture that wrote the output instead of asking the code
for it:

- Gap 1 wrote `amount` by hand, so every share came out flat and the
  roster's deviation channel had nothing to show. It now runs the real
  `recalculate`, with shares from 0.25 to 3.00 and one exclusion.
- Gap 2 seeded action names absent from `PARTS`, so every Details cell
  rendered the generic key=value fallback — precisely the rendering
  that column was rebuilt to avoid. The shot proved nothing about the
  code that ships.

Gap 1 closes clean: enumeration on that roster is warranted, item 12's
fix holds against a real split. Gaps 2 and 4 produce four findings,
recorded in SYNTHESIS.md — the largest being that `/payouts/new`'s
rejection notice pushes the form 88px down, measured, and only when it
has something to say. Nothing in `src/` is touched; the Phase 4 gate
still stands.

The spec is preserved as `capture-gaps-124.spec.ts.txt` beside
`capture.spec.ts.txt`, same convention, so any of it can be re-shot.
`.form-stack` already reserved the notice slot out of flow, so an empty
reservation cost nothing and the header sat flush — measured at 0px. But
a populated `Notice` renders `.notice`, a different class from the empty
`.notice-slot`, so the reset that flattened `.rule-head`'s 48px top
margin stopped matching at exactly the moment there was something to
say. Three spacings then stacked — the notice's own 24px bottom margin,
the grid's 16px row gap, and the header's 48px — and the distance went
0px empty, 88px populated.

Backwards: the one state where the error and the form it is about need
to read as one thing was the state that pushed them a third of a phone
screen apart, on /payouts/new, where vertical space is already scarce at
320px.

Two rules. The reset gains a `.notice` branch. And `.notice`'s own
bottom margin — written for flow layout, where it is the only thing
separating the notice from what follows — is zeroed inside `.form-stack`,
where the row gap already does that job. That second half is not
specific to the header: all four forms with a `.form-stack` mount their
`Notice` unconditionally, so add-participant, appraise and flat-pool
each had 40px where every other sibling pair sits at 16px. The header
case was just the loudest.

The test asserts a distance rather than a computed style, because the
defect was the sum and no single declaration was wrong on its own; the
expected value is the form's own row gap, read off the container. It
also pins the empty case at 0px, so a "fix" that reintroduces the
reservation's spacing fails too. Removing either rule alone fails it —
48px with the reset branch gone, 24px with the margin restored.
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 16 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c5eefc19-f286-4160-8d90-1dbae49ae4ed

📥 Commits

Reviewing files that changed from the base of the PR and between 5e8e118 and eeb63cd.

📒 Files selected for processing (8)
  • e2e/access-lists.spec.ts
  • e2e/audit.spec.ts
  • e2e/payouts.spec.ts
  • src/app/admin/access-lists/page.tsx
  • src/app/admin/access-lists/view.ts
  • src/app/admin/audit/page.tsx
  • src/app/globals.css
  • src/app/payouts/[id]/payment-history.tsx
📝 Walkthrough

Walkthrough

This change adds Design Sweep 2 documentation and capture fixtures. It also updates accessibility, responsive layouts, form validation, payout flows, admin pages, error recovery, shared UI components, styling, and E2E coverage.

Changes

Design Sweep 2

Layer / File(s) Summary
Design guidance and review records
DESIGN.md, PRODUCT.md, docs/design-sweep-2/*, AGENTS.md
Adds the dispatch brief, synthesis, reports, capture suites, design-rule corrections, and test-validation guidance.
Shared UI and styling
src/app/_components/*, src/app/globals.css, src/app/layout.tsx
Updates notices, focus headings, submit behavior, label styling, responsive tables, contrast documentation, and viewport color.
Admin and account surfaces
src/app/admin/*, src/app/account/page.tsx, src/app/login/page.tsx
Refines access-list naming and layout, audit metadata and timestamps, sync controls, account artwork sizing, and grouped login scopes.
Payout flows
src/app/payouts/**/*, tests/payment-history.test.ts
Adds server-side future-date validation, persistent form errors, pending labels, responsive labels, prominent edit controls, and normalized payment-history summaries.
Regression coverage
e2e/*
Adds coverage for focus behavior, responsive layouts, accessibility labels, validation, metadata, boundary recovery, and control placement.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • guarzo/authGD#128 — Directly related design-sweep implementation across shared UI, pages, and tests.
  • guarzo/authGD#204 — Directly related access-list page and navigation refinements.
  • guarzo/authGD#124 — Directly related payout workflow and detail-page changes.

Poem

Screens are swept, and tables align,
Focus rings glow in measured lines.
Forms now speak when errors arise,
Payout states have clearer guise.
Through narrow halls, the layouts flow,
While tests keep watch on what they show.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title describes the design sweep but does not use the required Conventional Commit form or identify the user-visible effect. Rename it to a format such as feat(design): improve responsive layouts and accessibility.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly documents the scope, user impact, verification, and deferred findings, but it does not use all template headings explicitly.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch worktree-design-sweep-2026-08-10

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@DESIGN.md`:
- Around line 325-327: Update the transition-count statement in
docs/design-sweep-2/reports/record-contradiction.md to report ten declarations,
matching the ten transition declarations in src/app/globals.css; leave the
surrounding analysis unchanged.

In `@docs/design-sweep-2/BRIEF.md`:
- Around line 293-302: Include /admin/access-lists in the reviewed scope as a
route-enumeration addition. In docs/design-sweep-2/BRIEF.md lines 293-302,
replace the out-of-scope wording with that classification; in
docs/design-sweep-2/reports/access-lists-audit.md lines 3-7, remove the
out-of-scope classification; and in docs/design-sweep-2/SYNTHESIS.md lines
622-628, classify its findings as included review scope.

In `@docs/design-sweep-2/capture.spec.ts.txt`:
- Line 442: Update the account query assertion at the seeded account setup to
await db.select().from(account) and validate that the returned rows are
non-empty, rather than asserting the promise object is truthy.

In `@docs/design-sweep-2/reports/access-lists-critique.md`:
- Around line 3-5: Remove the stale “OUT-OF-SCOPE ADDITION” warning at the
beginning of the access-lists critique, and replace it with an accurate
statement that /admin/access-lists is included in the reviewed sweep, or omit
the banner entirely.
- Around line 252-256: Add the required attribute to the access-list select in
the relevant form within the page component, preserving the disabled, selected
empty placeholder option so native validation blocks submission until a real
list is chosen.

In `@docs/design-sweep-2/reports/audit-audit.md`:
- Around line 336-342: Update the discussion of NVDA and JAWS announcements in
the report to qualify it as an expectation based on accessible-name and
accessible-description computation, consistent with the stated lack of running
screen-reader validation; alternatively, add documented screen-reader evidence
supporting the claim.

In `@docs/design-sweep-2/reports/boundaries-audit.md`:
- Around line 127-134: Update finding 2 in the boundaries audit to reflect the
implemented retry handoff: remove the recommendation to focus `#main` before
reset() and remove the request to add the error-boundary success-path test,
since src/app/error.tsx already focuses the recovered page’s `#main` after unmount
and e2e/error-boundary.spec.ts already covers it.
- Around line 164-169: Move seenAt storage out of the remounted error component
in error.tsx, replacing the component-local useRef with a persistent owner or
capture at the error source. Preserve one ISO-8601 UTC timestamp per error
identity, retaining the first timestamp when no digest is available, across
reset()-triggered remounts.

In `@docs/design-sweep-2/reports/boundaries-critique.md`:
- Around line 80-118: The boundaries critique still contains obsolete focus-ring
claims and a recommendation already implemented by the existing
h1[tabindex="-1"] width rule. Update or remove the finding in the report,
including its claims about the missing ring and proposed fix, so it reflects the
current implementation before merging.

In `@docs/design-sweep-2/reports/login-audit.md`:
- Around line 372-380: Update the `.launch__foot` tracking declaration in the
CSS fix list to use the shared `var(--track-label)` token instead of the raw
`0.12em` value, and move this item out of “Contested” into the recommended CSS
fixes.

In `@docs/design-sweep-2/reports/payout-detail-audit.md`:
- Around line 188-232: Update the fix so every listed action has both feedback
paths covered: add suitable pendingLabel values to exclude, mark paid, revert,
remove, pool delete, finalize, unlock, roster replacement, and operation
deletion, and ensure refusal notifications reach a mounted ConfirmGroup or
explicit onRefused handler for each action. Pay particular attention to
pool-flow.tsx and the page-level roster and operation forms; alternatively
narrow the finding to only the controls the implementation actually covers.

In `@docs/design-sweep-2/reports/payout-detail-critique.md`:
- Around line 85-87: Update the 320px reflow discussion in the payout-detail
critique: either add a 320px capture and page-level reflow assertion to
capture.spec.ts.txt, then report its measured result, or mark the case as
unmeasured and remove the unsupported “where it will be worse” statement.

In `@docs/design-sweep-2/reports/payouts-audit.md`:
- Around line 11-18: Reconcile the payout counts across the audit report,
critique, and capture fixture by identifying the fixture version used for
04-payouts-full. Update the reported finalized-row and amber-token totals in the
affected report sections to match that fixture, or explicitly name the version
when retaining differing distributions; ensure all references consistently
describe the same 34-operation dataset.

In `@docs/design-sweep-2/reports/payouts-new-critique.md`:
- Around line 66-79: Correct the `rows={4}` / `rows={3}` alternative in the
payout critique so it no longer claims the change puts Create above the fold at
1440×900. Describe it as only a partial height reduction, or replace the claim
with an accurate measured outcome while preserving the Disclosure alternative
and its defaultOpen requirements.

In `@docs/design-sweep-2/SYNTHESIS.md`:
- Around line 7-8: Update the Phase 4 gate statement near the “Nothing in src/
has been modified” text to identify the baseline revision and clarify that the
claim applied to the Phase 3 snapshot, or replace it with the final disposition
based on the committed tree.

In `@e2e/audit.spec.ts`:
- Around line 1000-1008: Update the audit test around the actor-filter and retry
flow to assert the seeded character.linked audit entry and the expected
target-filtered entry content rather than relying on tbody row counts. Retain
the separate .log__empty assertion, since the empty state is also rendered as a
table row, and verify the retry result specifically reflects the actor-to-target
link.

In `@e2e/payouts.spec.ts`:
- Line 1108: Update the test date setup around the tomorrow constant to use a
date at least two UTC days in the future, ensuring it remains future-dated even
if the test crosses UTC midnight; leave the existing ISO date formatting intact.

In `@src/app/admin/audit/page.tsx`:
- Around line 52-58: Update generateMetadata in the audit page to use the shared
one() repeated-parameter behavior rather than the local helper that selects the
first value, so metadata matches the page body for repeated before parameters.
Add an E2E assertion covering ?before=abc&before=123 and the resulting paged
title.

In `@src/app/payouts/actions.ts`:
- Around line 260-280: Update createOperationAction to validate the complete
FormData payload through a single Zod schema before calling appraiseLoot or
createOperationWithContents. Move the occurredAt parsing and future-date
refinement into that schema, include every other action input currently passed
to the services, and return the existing validation failure shape when parsing
fails; remove the manual occurredAt-only validation path.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a06d4a18-12fc-4efd-84a0-d090d6020606

📥 Commits

Reviewing files that changed from the base of the PR and between e495587 and 5e8e118.

📒 Files selected for processing (65)
  • .gitignore
  • AGENTS.md
  • DESIGN.md
  • PRODUCT.md
  • docs/design-sweep-2/BRIEF.md
  • docs/design-sweep-2/SYNTHESIS.md
  • docs/design-sweep-2/capture-gaps-124.spec.ts.txt
  • docs/design-sweep-2/capture.spec.ts.txt
  • docs/design-sweep-2/reports/access-lists-audit.md
  • docs/design-sweep-2/reports/access-lists-critique.md
  • docs/design-sweep-2/reports/audit-audit.md
  • docs/design-sweep-2/reports/audit-critique.md
  • docs/design-sweep-2/reports/boundaries-audit.md
  • docs/design-sweep-2/reports/boundaries-critique.md
  • docs/design-sweep-2/reports/login-audit.md
  • docs/design-sweep-2/reports/login-critique.md
  • docs/design-sweep-2/reports/payout-detail-audit.md
  • docs/design-sweep-2/reports/payout-detail-critique.md
  • docs/design-sweep-2/reports/payouts-audit.md
  • docs/design-sweep-2/reports/payouts-critique.md
  • docs/design-sweep-2/reports/payouts-new-audit.md
  • docs/design-sweep-2/reports/payouts-new-critique.md
  • docs/design-sweep-2/reports/record-contradiction.md
  • docs/design-sweep-2/reports/sync-audit.md
  • docs/design-sweep-2/reports/sync-critique.md
  • docs/design-sweep-2/reports/whole-app.md
  • docs/design-sweep-2/skill-update.patch
  • e2e/access-lists.spec.ts
  • e2e/account.spec.ts
  • e2e/audit.spec.ts
  • e2e/error-boundary.spec.ts
  • e2e/login.spec.ts
  • e2e/not-found.spec.ts
  • e2e/payouts.spec.ts
  • e2e/sync.spec.ts
  • src/app/_components/confirm-notice.tsx
  • src/app/_components/confirm-submit.tsx
  • src/app/_components/focus-heading.tsx
  • src/app/_components/nav-items.ts
  • src/app/_components/submit-guard.ts
  • src/app/_components/submit.tsx
  • src/app/_components/ui.tsx
  • src/app/account/page.tsx
  • src/app/admin/access-lists/page.tsx
  • src/app/admin/access-lists/view.ts
  • src/app/admin/accounts/page.tsx
  • src/app/admin/audit/page.tsx
  • src/app/admin/sync/page.tsx
  • src/app/admin/sync/view.ts
  • src/app/error.tsx
  • src/app/globals.css
  • src/app/layout.tsx
  • src/app/login/page.tsx
  • src/app/payouts/[id]/inline-edit.tsx
  • src/app/payouts/[id]/lifecycle-submit.tsx
  • src/app/payouts/[id]/page.tsx
  • src/app/payouts/[id]/pay-flow.tsx
  • src/app/payouts/[id]/payment-history.tsx
  • src/app/payouts/[id]/pool-flow.tsx
  • src/app/payouts/actions.ts
  • src/app/payouts/errors.ts
  • src/app/payouts/new/new-operation-form.tsx
  • src/app/payouts/new/page.tsx
  • src/app/payouts/page.tsx
  • tests/payment-history.test.ts

Comment thread DESIGN.md
Comment on lines +293 to +302
### 8. `/admin/access-lists` — **register: product**

- Source: `src/app/admin/access-lists/page.tsx`
- Shots: `14-access-lists.*`
- **This surface appears in neither the owner's scope list nor the Aug-5 sweep.**
It has never been reviewed. Mark your report as covering an out-of-scope
addition so its findings can be separated cleanly if the owner does not want
them.
- Report: `docs/design-sweep-2/reports/access-lists-critique.md` /
`access-lists-audit.md`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Treat /admin/access-lists as included scope.

The updated sweep rule says route enumeration adds an omitted surface to the review. Do not retain an out-of-scope banner for this surface.

  • docs/design-sweep-2/BRIEF.md#L293-L302: describe the surface as added by route enumeration.
  • docs/design-sweep-2/reports/access-lists-audit.md#L3-L7: remove the out-of-scope classification.
  • docs/design-sweep-2/SYNTHESIS.md#L622-L628: classify the findings as included review scope.
📍 Affects 3 files
  • docs/design-sweep-2/BRIEF.md#L293-L302 (this comment)
  • docs/design-sweep-2/reports/access-lists-audit.md#L3-L7
  • docs/design-sweep-2/SYNTHESIS.md#L622-L628
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/design-sweep-2/BRIEF.md` around lines 293 - 302, Include
/admin/access-lists in the reviewed scope as a route-enumeration addition. In
docs/design-sweep-2/BRIEF.md lines 293-302, replace the out-of-scope wording
with that classification; in docs/design-sweep-2/reports/access-lists-audit.md
lines 3-7, remove the out-of-scope classification; and in
docs/design-sweep-2/SYNTHESIS.md lines 622-628, classify its findings as
included review scope.

await page.goto("/admin/accounts");
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
await shoot(page, "15-admin-accounts");
await expect(db.select().from(account)).toBeTruthy();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Await and assert the seeded account rows.

Line 442 asserts that a promise object is truthy. It does not execute or validate the query result. The capture can pass after broken account setup. Await the query and assert a non-empty result.

Proposed fix
-  await expect(db.select().from(account)).toBeTruthy();
+  const accounts = await db.select().from(account);
+  expect(accounts).not.toHaveLength(0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await expect(db.select().from(account)).toBeTruthy();
const accounts = await db.select().from(account);
expect(accounts).not.toHaveLength(0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/design-sweep-2/capture.spec.ts.txt` at line 442, Update the account
query assertion at the seeded account setup to await db.select().from(account)
and validate that the returned rows are non-empty, rather than asserting the
promise object is truthy.

Comment on lines +3 to +5
> **OUT-OF-SCOPE ADDITION.** This surface appears in neither the owner's scope
> list for this sweep nor the Aug-5 sweep. It has never been reviewed. Every
> finding below can be lifted out cleanly if the owner does not want them.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the stale out-of-scope warning.

The PR objectives explicitly include /admin/access-lists in this sweep. This banner says the surface was not reviewed and may cause maintainers to discard valid findings. Replace it with the intended scope statement or remove it.

As per PR objectives, /admin/access-lists is one of the reviewed surfaces.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/design-sweep-2/reports/access-lists-critique.md` around lines 3 - 5,
Remove the stale “OUT-OF-SCOPE ADDITION” warning at the beginning of the
access-lists critique, and replace it with an accurate statement that
/admin/access-lists is included in the reviewed sweep, or omit the banner
entirely.

Comment thread docs/design-sweep-2/reports/access-lists-critique.md
Comment on lines +7 to +8
**Nothing in `src/` has been modified.** This document is the Phase 3 output and
the Phase 4 gate: the owner chooses what gets worked before any edit.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Pin the Phase 3 source snapshot.

src/ changes in this PR. These lines are false when read against the committed tree. Name the baseline revision and state that this was true at the Phase 3 snapshot, or replace the gate language with the final disposition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/design-sweep-2/SYNTHESIS.md` around lines 7 - 8, Update the Phase 4 gate
statement near the “Nothing in src/ has been modified” text to identify the
baseline revision and clarify that the claim applied to the Phase 3 snapshot, or
replace it with the final disposition based on the committed tree.

Comment thread e2e/audit.spec.ts Outdated
Comment thread e2e/payouts.spec.ts Outdated
Comment thread src/app/admin/audit/page.tsx Outdated
Comment on lines +260 to +280
// `max={today}` on the form declares this rule; until now the browser was the
// only thing enforcing it, so a hand-built request could always date an
// operation into the future. That became load-bearing the moment this form
// took `noValidate` (see new-operation-form.tsx): with native validation off,
// the attribute stops being enforcement at all and this check is the rule.
//
// Compared against the same UTC-midnight boundary `parseYmd` produces, not
// against `now`: both sides are then EVE-day granular, so an operation
// recorded during today's downtime is not rejected for being "ahead" of an
// instant a few hours later in the same day.
//
// `/payouts/[id]`'s own date field is deliberately NOT changed to match. It
// still runs native validation, so its `max={today}` still holds for anyone
// using the page; it carries the same scripted-request gap this closes here,
// pre-existing and unchanged, and closing it there means a new code on
// `OPERATION_ERRORS` for a path this task did not touch.
const todayUtc = new Date();
todayUtc.setUTCHours(0, 0, 0, 0);
if (occurredAt.getTime() > todayUtc.getTime()) {
return { ok: false, code: "date_future" };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Validate the complete form with Zod before service calls.

createOperationAction manually parses occurredAt and continues with other unvalidated FormData fields. Parse one Zod schema, including the future-date refinement, before appraiseLoot and createOperationWithContents.

As per path instructions, “Server actions must validate their input with zod before touching a service.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/payouts/actions.ts` around lines 260 - 280, Update
createOperationAction to validate the complete FormData payload through a single
Zod schema before calling appraiseLoot or createOperationWithContents. Move the
occurredAt parsing and future-date refinement into that schema, include every
other action input currently passed to the services, and return the existing
validation failure shape when parsing fails; remove the manual occurredAt-only
validation path.

Source: Path instructions

…-2026-08-10

# Conflicts:
#	e2e/access-lists.spec.ts
#	src/app/admin/access-lists/page.tsx
@guarzo

guarzo commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 24 minutes.

`generateMetadata` had its own `one()` that took the FIRST value of a
repeated query param, shadowing the module's, which takes the last. On
`?before=abc&before=99999999` the body paged on the numeric cursor while
the title announced an unpaged log — and on this surface the title IS the
announcement, because every control that changes the result set is a
document load. The `paged` guard exists precisely to stop the title
claiming a page the admin is not on, and the shadow defeated it.

Also two test fixes CodeRabbit was right about:

- The future-date test built its date 24h out and the action compares
  against UTC midnight of the instant it runs, so a run that crossed the
  boundary between fill and submit was posting today and passing. 48h.
- The actor-filter test asserted row cardinality only, which any single
  row satisfies. It now names which entries it expects on each side of
  the nudge, which is the behaviour the test is about.
@guarzo
guarzo merged commit 958120f into main Aug 10, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant